{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Copyright (c) Microsoft Corporation. All rights reserved.\n",
        "Licensed under the MIT License."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "![Impressions](https://PixelServer20190423114238.azurewebsites.net/api/impressions/MachineLearningNotebooks/how-to-use-azureml/machine-learning-pipelines/parallel-run/tabular-dataset-inference-iris.png)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Using Azure Machine Learning Pipelines for Batch Inference for CSV Files\n",
        "\n",
        "In this notebook, we will demonstrate how to make predictions on large quantities of data asynchronously using the ML pipelines with Azure Machine Learning. Batch inference (or batch scoring) provides cost-effective inference, with unparalleled throughput for asynchronous applications. Batch prediction pipelines can scale to perform inference on terabytes of production data. Batch prediction is optimized for high throughput, fire-and-forget predictions for a large collection of data.\n",
        "\n",
        "> **Tip**\n",
        "If your system requires low-latency processing (to process a single document or small set of documents quickly), use [real-time scoring](https://docs.microsoft.com/azure/machine-learning/v1/how-to-consume-web-service) instead of batch prediction.\n",
        "\n",
        "In this example we will take use a machine learning model already trained to predict different types of iris flowers and run that trained model on some of the data in a CSV file which has characteristics of different iris flowers.  However, the same example can be extended to manipulating data to any embarrassingly-parallel processing through a python script.\n",
        "\n",
        "The outline of this notebook is as follows:\n",
        "\n",
        "- Create a DataStore referencing the CSV files stored in a blob container.\n",
        "- Register the pretrained model into the model registry. \n",
        "- Use the registered model to do batch inference on the CSV files in the data blob container.\n",
        "\n",
        "## Prerequisites\n",
        "If you are using an Azure Machine Learning Notebook VM, you are all set. Otherwise, make sure you go through the configuration Notebook located at https://github.com/Azure/MachineLearningNotebooks first. This sets you up with a working config file that has information on your workspace, subscription id, etc. \n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### Connect to workspace\n",
        "Create a workspace object from the existing workspace. Workspace.from_config() reads the file config.json and loads the details into an object named ws."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "tags": []
      },
      "outputs": [],
      "source": [
        "# Check core SDK version number\n",
        "import azureml.core\n",
        "\n",
        "print(\"SDK version:\", azureml.core.VERSION)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "tags": []
      },
      "outputs": [],
      "source": [
        "from azureml.core import Workspace\n",
        "\n",
        "ws = Workspace.from_config()\n",
        "print('Workspace name: ' + ws.name, \n",
        "      'Azure region: ' + ws.location, \n",
        "      'Subscription id: ' + ws.subscription_id, \n",
        "      'Resource group: ' + ws.resource_group, sep = '\\n')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### Create or Attach existing compute resource\n",
        "By using Azure Machine Learning Compute, a managed service, data scientists can train machine learning models on clusters of Azure virtual machines. Examples include VMs with GPU support. In this tutorial, you create Azure Machine Learning Compute as your training environment. The code below creates the compute clusters for you if they don't already exist in your workspace.\n",
        "\n",
        "> Note that if you have an AzureML Data Scientist role, you will not have permission to create compute resources. Talk to your workspace or IT admin to create the compute targets described in this section, if they do not already exist.\n",
        "\n",
        "**Creation of compute takes approximately 5 minutes. If the AmlCompute with that name is already in your workspace the code will skip the creation process.**"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "import os\n",
        "from azureml.core.compute import AmlCompute, ComputeTarget\n",
        "\n",
        "# choose a name for your cluster\n",
        "compute_name = os.environ.get(\"AML_COMPUTE_CLUSTER_NAME\", \"cpu-cluster\")\n",
        "compute_min_nodes = os.environ.get(\"AML_COMPUTE_CLUSTER_MIN_NODES\", 0)\n",
        "compute_max_nodes = os.environ.get(\"AML_COMPUTE_CLUSTER_MAX_NODES\", 4)\n",
        "\n",
        "# This example uses CPU VM. For using GPU VM, set SKU to Standard_NC6s_v3\n",
        "vm_size = os.environ.get(\"AML_COMPUTE_CLUSTER_SKU\", \"STANDARD_D2_V2\")\n",
        "\n",
        "\n",
        "if compute_name in ws.compute_targets:\n",
        "    compute_target = ws.compute_targets[compute_name]\n",
        "    if compute_target and type(compute_target) is AmlCompute:\n",
        "        print('found compute target. just use it. ' + compute_name)\n",
        "else:\n",
        "    print('creating a new compute target...')\n",
        "    provisioning_config = AmlCompute.provisioning_configuration(vm_size = vm_size,\n",
        "                                                                min_nodes = compute_min_nodes, \n",
        "                                                                max_nodes = compute_max_nodes)\n",
        "\n",
        "    # create the cluster\n",
        "    compute_target = ComputeTarget.create(ws, compute_name, provisioning_config)\n",
        "    \n",
        "    # can poll for a minimum number of nodes and for a specific timeout.\n",
        "    # if no min node count is provided it will use the scale settings for the cluster\n",
        "    compute_target.wait_for_completion(show_output=True, min_node_count=None, timeout_in_minutes=20)\n",
        "    \n",
        "     # For a more detailed view of current AmlCompute status, use get_status()\n",
        "    print(compute_target.get_status().serialize())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### Create a datastore containing sample images\n",
        "The input dataset used for this notebook is CSV data which has attributes of different iris flowers. We have created a public blob container `sampledata` on an account named `pipelinedata`, containing iris data set. In the next step, we create a datastore with the name `iris_datastore`, which points to this container. In the call to `register_azure_blob_container` below, setting the `overwrite` flag to `True` overwrites any datastore that was created previously with that name. \n",
        "\n",
        "This step can be changed to point to your blob container by providing your own `datastore_name`, `container_name`, and `account_name`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "from azureml.core.datastore import Datastore\n",
        "\n",
        "account_name = \"pipelinedata\"\n",
        "datastore_name=\"iris_datastore_data\"\n",
        "container_name=\"sampledata\"\n",
        "\n",
        "iris_data = Datastore.register_azure_blob_container(ws, \n",
        "                      datastore_name=datastore_name, \n",
        "                      container_name= container_name, \n",
        "                      account_name=account_name, \n",
        "                      overwrite=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### Create a TabularDataset\n",
        "A [TabularDataSet](https://docs.microsoft.com/python/api/azureml-core/azureml.data.tabulardataset?view=azure-ml-py) references single or multiple files which contain data in a tabular structure (ie like CSV files) in your datastores or public urls. TabularDatasets provides you with the ability to download or mount the files to your compute. By creating a dataset, you create a reference to the data source location. If you applied any subsetting transformations to the dataset, they will be stored in the dataset as well. The data remains in its existing location, so no extra storage cost is incurred.\n",
        "You can use dataset objects as inputs. Register the datasets to the workspace if you want to reuse them later."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "from azureml.core.dataset import Dataset\n",
        "\n",
        "iris_ds_name = 'iris_data'\n",
        "\n",
        "path_on_datastore = iris_data.path('iris/')\n",
        "input_iris_ds = Dataset.Tabular.from_delimited_files(path=path_on_datastore, validate=False)\n",
        "named_iris_ds = input_iris_ds.as_named_input(iris_ds_name)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### Intermediate/Output Data\n",
        "Intermediate data (or output of a Step) is represented by [PipelineData](https://docs.microsoft.com/python/api/azureml-pipeline-core/azureml.pipeline.core.pipelinedata?view=azure-ml-py) object. PipelineData can be produced by one step and consumed in another step by providing the PipelineData object as an output of one step and the input of one or more steps."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "from azureml.pipeline.core import PipelineData\n",
        "\n",
        "datastore = ws.get_default_datastore()\n",
        "output_folder = PipelineData(name='inferences', datastore=datastore)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Registering the Model with the Workspace\n",
        "Get the pretrained model from a publicly available Azure Blob container, then register it to use in your workspace"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "model_container_name=\"iris-model\"\n",
        "model_datastore_name=\"iris_model_datastore\"\n",
        "\n",
        "model_datastore = Datastore.register_azure_blob_container(ws, \n",
        "                      datastore_name=model_datastore_name, \n",
        "                      container_name= model_container_name, \n",
        "                      account_name=account_name, \n",
        "                      overwrite=True)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "from azureml.core.model import Model\n",
        "\n",
        "model_datastore.download('iris_model.pkl')\n",
        "\n",
        "# register downloaded model\n",
        "model = Model.register(model_path = \"iris_model.pkl/iris_model.pkl\",\n",
        "                       model_name = \"iris-prs\", # this is the name the model is registered as\n",
        "                       tags = {'pretrained': \"iris\"},\n",
        "                       workspace = ws)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### Using your model to make batch predictions\n",
        "To use the model to make batch predictions, you need an **entry script** and a list of **dependencies**:\n",
        "\n",
        "#### An entry script\n",
        "This script accepts requests, scores the requests by using the model, and returns the results.\n",
        "- __init()__ - Typically this function loads the model into a global object. This function is run only once at the start of batch processing per worker node/process. init method can make use of following environment variables (ParallelRunStep input):\n",
        "    1.\tAZUREML_BI_OUTPUT_PATH - output folder path\n",
        "- __run(mini_batch)__ - The method to be parallelized. Each invocation will have one minibatch.<BR>\n",
        "__mini_batch__: Batch inference will invoke run method and pass either a list or Pandas DataFrame as an argument to the method. Each entry in min_batch will be - a filepath if input is a FileDataset, a Pandas DataFrame if input is a TabularDataset.<BR>\n",
        "__run__ method response: run() method should return a Pandas DataFrame or an array. For append_row output_action, these returned elements are appended into the common output file. For summary_only, the contents of the elements are ignored. For all output actions, each returned output element indicates one successful inference of input element in the input mini-batch.\n",
        "    User should make sure that enough data is included in inference result to map input to inference. Inference output will be written in output file and not guaranteed to be in order, user should use some key in the output to map it to input.\n",
        "    \n",
        "\n",
        "#### Dependencies\n",
        "Helper scripts or Python/Conda packages required to run the entry script.\n",
        "\n",
        "## Print inferencing script"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "scripts_folder = \"Code\"\n",
        "script_file = \"iris_score.py\"\n",
        "\n",
        "# peek at contents\n",
        "with open(os.path.join(scripts_folder, script_file)) as inference_file:\n",
        "    print(inference_file.read())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Build and run the batch inference pipeline\n",
        "The data, models, and compute resource are now available. Let's put all these together in a pipeline."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "###  Specify the environment to run the script\n",
        "Specify the conda dependencies for your script. This will allow us to install pip packages as well as configure the inference environment.\n",
        "* Always include **azureml-core** and **azureml-dataset-runtime\\[fuse\\]** in the pip package list to make ParallelRunStep run properly.\n",
        "* For TabularDataset, add **pandas** as `run(mini_batch)` uses `pandas.DataFrame` as mini_batch type.\n",
        "\n",
        "If you're using custom image (`batch_env.python.user_managed_dependencies = True`), you need to install the package to your image."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "from azureml.core import Environment\n",
        "from azureml.core.runconfig import CondaDependencies\n",
        "\n",
        "predict_conda_deps = CondaDependencies.create(python_version=\"3.8\", \n",
        "                                              conda_packages=['pip==20.2.4'],\n",
        "                                              pin_sdk_version=False,\n",
        "                                              pip_packages=[\"numpy==1.19.5\", \"pandas==1.4.4\", \"scikit-learn==0.22.2\",\n",
        "                                                            \"azureml-core==1.54.0\", \"azureml-dataset-runtime[fuse]==1.54.0\"])\n",
        "\n",
        "predict_env = Environment(name=\"predict_environment\")\n",
        "predict_env.python.conda_dependencies = predict_conda_deps\n",
        "predict_env.spark.precache_packages = False"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "###  Create the configuration to wrap the inference script"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "from azureml.pipeline.steps import ParallelRunStep, ParallelRunConfig\n",
        "\n",
        "# In a real-world scenario, you'll want to shape your process per node and nodes to fit your problem domain.\n",
        "parallel_run_config = ParallelRunConfig(\n",
        "    source_directory=scripts_folder,\n",
        "    entry_script=script_file,  # the user script to run against each input\n",
        "    mini_batch_size='1KB',\n",
        "    error_threshold=5,\n",
        "    output_action='append_row',\n",
        "    append_row_file_name=\"iris_outputs.txt\",\n",
        "    environment=predict_env,\n",
        "    compute_target=compute_target, \n",
        "    node_count=2,\n",
        "    run_invocation_timeout=600\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### Create the pipeline step\n",
        "Create the pipeline step using the script, environment configuration, and parameters. Specify the compute target you already attached to your workspace as the target of execution of the script. We will use ParallelRunStep to create the pipeline step."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "distributed_csv_iris_step = ParallelRunStep(\n",
        "    name='example-iris',\n",
        "    inputs=[named_iris_ds],\n",
        "    output=output_folder,\n",
        "    parallel_run_config=parallel_run_config,\n",
        "    arguments=['--model_name', 'iris-prs'],\n",
        "    allow_reuse=False\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### Run the pipeline\n",
        "At this point you can run the pipeline and examine the output it produced. The Experiment object is used to track the run of the pipeline"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "from azureml.core import Experiment\n",
        "from azureml.pipeline.core import Pipeline\n",
        "\n",
        "pipeline = Pipeline(workspace=ws, steps=[distributed_csv_iris_step])\n",
        "\n",
        "pipeline_run = Experiment(ws, 'iris-prs').submit(pipeline)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## View progress of Pipeline run\n",
        "\n",
        "The pipeline run status could be checked in Azure Machine Learning portal (https://ml.azure.com). The link to the pipeline run could be retrieved by inspecting the `pipeline_run` object."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# This will output information of the pipeline run, including the link to the details page of portal.\n",
        "pipeline_run"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### Optional: View detailed logs (streaming) "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "tags": []
      },
      "outputs": [],
      "source": [
        "## Wait the run for completion and show output log to console\n",
        "pipeline_run.wait_for_completion(show_output=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## View Results\n",
        "In the iris_score.py file above you can see that the Result with the prediction of the iris variety gets returned and then appended to the original input of the row from the csv file. These results are written to the DataStore specified in the PipelineData object as the output data, which in this case is called *inferences*. This contains the outputs from  all of the worker nodes used in the compute cluster. You can download this data to view the results ... below just filters to a random 20 rows"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "import pandas as pd\n",
        "import tempfile\n",
        "\n",
        "prediction_run = pipeline_run.find_step_run(distributed_csv_iris_step.name)[0]\n",
        "prediction_output = prediction_run.get_output_data(output_folder.name)\n",
        "\n",
        "target_dir = tempfile.mkdtemp()\n",
        "prediction_output.download(local_path=target_dir)\n",
        "result_file = os.path.join(target_dir, prediction_output.path_on_datastore, parallel_run_config.append_row_file_name)\n",
        "\n",
        "# cleanup output format\n",
        "df = pd.read_csv(result_file, delimiter=\" \", header=None)\n",
        "df.columns = [\"sepal.length\", \"sepal.width\", \"petal.length\", \"petal.width\", \"variety\"]\n",
        "print(\"Prediction has \", df.shape[0], \" rows\")\n",
        "\n",
        "random_subset = df.sample(n=20)\n",
        "random_subset.head(20)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Cleanup compute resources\n",
        "For re-occurring jobs, it may be wise to keep compute the compute resources and allow compute nodes to scale down to 0. However, since this is just a single run job, we are free to release the allocated compute resources."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# uncomment below and run if compute resources are no longer needed \n",
        "# compute_target.delete()"
      ]
    }
  ],
  "metadata": {
    "authors": [
      {
        "name": "prsbjdev"
      }
    ],
    "category": "Other notebooks",
    "compute": [
      "AML Compute"
    ],
    "datasets": [
      "IRIS"
    ],
    "deployment": [
      "None"
    ],
    "exclude_from_index": false,
    "framework": [
      "None"
    ],
    "friendly_name": "IRIS data inferencing using ParallelRunStep",
    "index_order": 1,
    "kernelspec": {
      "display_name": "Python 3.8 - AzureML",
      "language": "python",
      "name": "python38-azureml"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.8.16"
    },
    "tags": [
      "Batch Inferencing",
      "Pipeline"
    ],
    "task": "Recognize flower type"
  },
  "nbformat": 4,
  "nbformat_minor": 2
}